package com.nineclock.attendance.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.nineclock.attendance.dto.AttendGroupDTO;
import com.nineclock.attendance.dto.AttendPunchDTO;
import com.nineclock.attendance.dto.AttendPunchUserWholeDayDTO;
import com.nineclock.attendance.dto.AttendsDTO;
import com.nineclock.attendance.enums.AttendEnums;
import com.nineclock.attendance.excel.CustomStyle;
import com.nineclock.attendance.mapper.AttendPunchMapper;
import com.nineclock.attendance.pojo.AttendPunch;
import com.nineclock.attendance.service.AttendGroupService;
import com.nineclock.attendance.service.AttendPunchService;
import com.nineclock.common.enums.ResponseEnum;
import com.nineclock.common.exception.NcException;
import com.nineclock.common.threadlocal.CurrentUserHolder;
import com.nineclock.common.utils.BeanHelper;
import com.nineclock.common.utils.DateTimeUtil;
import com.nineclock.common.utils.DistanceUtil;
import com.nineclock.system.dto.SysCompanyUserDTO;
import com.nineclock.system.feign.SysUserFeign;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.parameters.P;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

@Service
@Transactional
public class AttendPunchServiceImpl implements AttendPunchService {

    @Autowired
    private AttendPunchMapper attendPunchMapper;

    @Autowired
    private AttendGroupService attendGroupService;

    //打卡
    @Override
    public void punch(AttendPunchDTO punchDTO) {
        //1. 参数校验
        if(punchDTO == null){
            throw new NcException(ResponseEnum.INVALID_PARAM_ERROR);
        }


        //2. 封装打卡信息
        Date now = new Date();
        AttendPunch attendPunch = BeanHelper.copyProperties(punchDTO, AttendPunch.class);
        attendPunch.setPunchTime(now);
        attendPunch.setCreateTime(now);
        attendPunch.setCompanyId(CurrentUserHolder.get().getCompanyId());
        attendPunch.setCompanyUserId(CurrentUserHolder.get().getCompanyUserId());

        //2.1 设置打卡时间
        attendPunch.setPunchDateStr(DateTimeUtil.dateToStr(now, DateTimeUtil.TIME_FORMAT_2));

        //2.2 判断上下午打卡 ---> 数据库存储是 1 : 上午 , 2 : 下午
        int noonType = DateTimeUtil.noonType(now);
        attendPunch.setPunchOnOffWork(noonType + 1);



        //3. 获取到用户考勤组
        AttendGroupDTO attendGroup = attendGroupService.queryAttendGroup();
        //3.1 判定考勤组
        if(attendGroup == null){
            throw new NcException(ResponseEnum.USER_NOT_MATCH_ATTENDGROUP);
        }


        //4. 判断是否为有效打卡
        //4.1 判断打卡是否在有效范围内 ---> true: 在范围之内 ; false: 超出范围
        boolean flag = this.validateArea(punchDTO,attendGroup);
        if(!flag){
            throw new NcException(ResponseEnum.PUNCH_INVALID_AREA);
        }
        attendPunch.setAreaValid(true);


        //4.2 判断是否为工作日打卡 ----> 非工作日不用打卡 ====> 判断是否为工作日打卡  [1,1,1,1,1,0,0]
        boolean isWorkDay = validateWorkDay(attendGroup);
        if(!isWorkDay){
            throw new NcException(ResponseEnum.PUNCH_INVALID_DAY);
        }
        attendPunch.setDayType(1);


        //4.3 判断是否为有效打卡 ------> 上午第一次 , 下午最后一次
        boolean effective = validateEffective(attendPunch, attendGroup);
        if(effective){
            setPunchType(attendPunch, attendGroup); //标记打卡类型 ---> 正常 , 迟到 , 旷工 , 早退
        }

        //4.4 保存打卡数据
        attendPunchMapper.insert(attendPunch);
    }



    //标记当前这次打卡 是 正常 / 迟到 / 早退 / 旷工 ==========> 时间比较
    private void setPunchType(AttendPunch attendPunch, AttendGroupDTO attendGroupDto) {

        //获取考勤组中的 --- 允许迟到分钟数 10
        Integer allowLateMinutes = attendGroupDto.getAllowLateMinutes();
        //获取考勤组中的 ---- 旷工迟到分钟数 60
        Integer lateMinutes = attendGroupDto.getLateMinutes();

        //获取 实际打卡时间 字符串
        Date punchTime = attendPunch.getPunchTime();
        String punchTimeStr = DateTimeUtil.dateToStr(punchTime, DateTimeUtil.TIME_FORMAT_1);

        //默认正常
        AttendEnums punchType = AttendEnums.PUNCH_TYPE_OK;

        //判定
        //上午
        if(attendPunch.getPunchOnOffWork() == AttendEnums.NOON_TYPE_MORNING.value()){
            //获取关联的考勤组的设置上班时间
            String startWorkTime = attendGroupDto.getStartWorkTime();

            //判定上班打卡时间
            if(DateTimeUtil.compareTime(punchTimeStr, startWorkTime, allowLateMinutes) == 1){ //返回1 , 说明 : punchTimeStr > startWorkTime + allowLateMinutes
                punchType = AttendEnums.PUNCH_TYPE_LATE;

                if(DateTimeUtil.compareTime(punchTimeStr, startWorkTime, lateMinutes) == 1){  //返回1 , 说明 : punchTimeStr > startWorkTime + lateMinutes
                    punchType = AttendEnums.PUNCH_TYPE_STAYALAY;
                }
            }

            attendPunch.setMorningPunchType(punchType.value());
            //下午
        }else{
            String offWorkTime = attendGroupDto.getOffWorkTime();

            //判定下班打卡时间
            if(DateTimeUtil.compareTime(offWorkTime, punchTimeStr, allowLateMinutes) == 1){ //返回1 , 说明 : offWorkTime > punchTimeStr + allowLateMinutes
                punchType = AttendEnums.PUNCH_TYPE_EARLY;
                if(DateTimeUtil.compareTime(punchTimeStr, offWorkTime, lateMinutes) == 1){  //返回1 , 说明 : punchTimeStr > startWorkTime + lateMinutes
                    punchType = AttendEnums.PUNCH_TYPE_STAYALAY;
                }
            }
            attendPunch.setAfternoonPunchType(punchType.value());
        }
    }



    /**
     * 判定是否为有效打卡
     *
     * 上午第一次是有效的 , 下午最后一次是有效的
     *
     * @param attendPunch
     * @param attendGroup
     * @return
     */
    private boolean validateEffective(AttendPunch attendPunch, AttendGroupDTO attendGroup) {
        //查询当天 , 上午/下午是否打过卡
        LambdaQueryWrapper<AttendPunch> punchWrapper = new LambdaQueryWrapper<>();
        punchWrapper.eq(AttendPunch::getCompanyId, CurrentUserHolder.get().getCompanyId())
                .eq(AttendPunch::getCompanyUserId, CurrentUserHolder.get().getCompanyUserId())
                .eq(AttendPunch::getPunchDateStr, attendPunch.getPunchDateStr())//打卡日期
                .eq(AttendPunch::getPunchOnOffWork, attendPunch.getPunchOnOffWork())//上午/下午
                .eq(AttendPunch::getAreaValid, true)//范围有效
                .eq(AttendPunch::getDayType, 1); //工作日打卡

        Integer count = attendPunchMapper.selectCount(punchWrapper);

        if(count > 0){//上午第一次是有效的 , 下午最后一次是有效的
            if(attendPunch.getPunchOnOffWork() == AttendEnums.NOON_TYPE_MORNING.value()){//上午
                attendPunch.setEffectiveValid(false);
            }else{ //下午
                //更新之前的打卡记录为无效 , 当次有效
                AttendPunch punch = new AttendPunch();
                punch.setEffectiveValid(false);
                attendPunchMapper.update(punch, punchWrapper); //更新之前的打卡记录为无效

                attendPunch.setEffectiveValid(true);
            }
        }else{
            attendPunch.setEffectiveValid(true);//有效
        }

        return attendPunch.getEffectiveValid();
    }


    private boolean validateWorkDay(AttendGroupDTO attendGroupDTO) {
        int week = DateUtil.thisDayOfWeek(); // 获取当前日期是星期几  ----> 1: 表示周日, 2: 表示周一, 3:表示周二

        int index = 0;
        List<String> workdays = attendGroupDTO.getWorkdays(); //-----> [1,1,1,1,1,0,0]
        if(week == 1){//周日
            index = 6;
        }else{
            index = week - 2;
        }

        boolean flag = Integer.valueOf(workdays.get(index)) == 1;
        return flag;
    }




    //判断打卡是否在有效范围内 ------> attendPunchDTO的打卡坐标 - attendGroupDto打卡坐标 <= 允许波动范围
    private boolean validateArea(AttendPunchDTO attendPunchDTO, AttendGroupDTO attendGroupDto) {
        //比较"打卡坐标" 和 "考勤组打卡坐标"的距离
        Integer meter = DistanceUtil.getDistanceMeter(attendPunchDTO.getLat().doubleValue(), attendPunchDTO.getLng().doubleValue(), attendGroupDto.getLat().doubleValue(), attendGroupDto.getLng().doubleValue());

        //判定是否小于等于考勤组中设置的范围
        return meter <= attendGroupDto.getAddressRange();
    }


    @Override
    public AttendPunchUserWholeDayDTO getPunchData() {
        //获取当前用户信息 , 当前时间
        Long companyUserId = CurrentUserHolder.get().getCompanyUserId();
        String nowStr = DateTimeUtil.dateToStr(new Date(), DateTimeUtil.TIME_FORMAT_2);


        //查询上午打卡记录
        LambdaQueryWrapper<AttendPunch> amWrapper = new LambdaQueryWrapper<>();
        amWrapper.eq(AttendPunch::getCompanyUserId, companyUserId);
        amWrapper.eq(AttendPunch::getPunchDateStr, nowStr);
        amWrapper.eq(AttendPunch::getPunchOnOffWork, AttendEnums.NOON_TYPE_MORNING.value());
        amWrapper.eq(AttendPunch::getEffectiveValid, true);

        AttendPunch amPunch = attendPunchMapper.selectOne(amWrapper);

        //查询下午打卡记录
        LambdaQueryWrapper<AttendPunch> pmWrapper = new LambdaQueryWrapper<>();
        pmWrapper.eq(AttendPunch::getCompanyUserId, companyUserId);
        pmWrapper.eq(AttendPunch::getPunchDateStr, nowStr);
        pmWrapper.eq(AttendPunch::getPunchOnOffWork, AttendEnums.NOON_TYPE_AFTERNOON.value());
        pmWrapper.eq(AttendPunch::getEffectiveValid, true);

        AttendPunch pmPunch = attendPunchMapper.selectOne(pmWrapper);


        //组装结果, 返回
        AttendPunchUserWholeDayDTO wholeDayDTO = new AttendPunchUserWholeDayDTO();
        if(amPunch != null){
            wholeDayDTO.setAttendPunchMorningResponse(BeanHelper.copyProperties(amPunch, AttendPunchDTO.class));
        }

        if(pmPunch != null){
            wholeDayDTO.setAttendPunchAfterNoonResponse(BeanHelper.copyProperties(pmPunch, AttendPunchDTO.class));
        }

        return wholeDayDTO;
    }

    @Autowired
    private SysUserFeign sysUserFeign;

    @Override
    public List<AttendPunchDTO> queryMembers(String startTime, String endTime) {
        //参数校验
        if(StrUtil.isEmpty(startTime) || StrUtil.isEmpty(endTime)){
            throw new NcException(ResponseEnum.INVALID_PARAM_ERROR);
        }

        // 获取指定时间范围内的时间列表
        List<Date> dateList = DateTimeUtil.getDatesBetweenTwoDate(startTime, endTime, DateTimeUtil.TIME_FORMAT_2);
        List<String> dateStrList = dateList.stream().map(date -> {
            return DateTimeUtil.dateToStr(date, DateTimeUtil.TIME_FORMAT_2);
        }).collect(Collectors.toList());


        //获取当前企业下所有的员工
        List<SysCompanyUserDTO> companyUserDTOS = sysUserFeign.queryAllCompanyUser().getData();


        //遍历员工集合, 获取每一个员工在指定时间段内的打卡情况
        List<AttendPunchDTO> attendPunchDTOList = getAttendPunchDTOS(startTime, endTime, dateStrList, companyUserDTOS);

        //组装数据并返回
        return attendPunchDTOList;
    }

    /**
     * 遍历员工集合, 获取每一个员工在指定时间段内的打卡情况
     * @param startTime  2021-04-01
     * @param endTime    2021-04-10
     * @param dateStrList
     * @param companyUserDTOS
     * @return
     */
    private List<AttendPunchDTO> getAttendPunchDTOS(String startTime, String endTime,
                                                    List<String> dateStrList, List<SysCompanyUserDTO> companyUserDTOS) {

        List<AttendPunchDTO> punchDTOS = companyUserDTOS.stream().map(sysCompanyUserDTO -> {

            //查询该员工的考勤打卡情况 -----> 员工ID , 时间范围 ;
            LambdaQueryWrapper<AttendPunch> pWrapper = new LambdaQueryWrapper<>();
            pWrapper.eq(AttendPunch::getCompanyUserId, sysCompanyUserDTO.getId());//员工ID
            pWrapper.eq(AttendPunch::getEffectiveValid, true);//有效
            pWrapper.between(AttendPunch::getPunchDateStr, startTime, endTime);//时间范围 ;

            List<AttendPunch> attendPunches = attendPunchMapper.selectList(pWrapper);

            //封装当前员工的考勤打卡数据
            AttendPunchDTO attendPunchDTO = new AttendPunchDTO();
            attendPunchDTO.setUserName(sysCompanyUserDTO.getUserName());
            attendPunchDTO.setWorkNumber(sysCompanyUserDTO.getWorkNumber());
            attendPunchDTO.setPost(sysCompanyUserDTO.getPost());
            attendPunchDTO.setDepartmentName(sysCompanyUserDTO.getDepartmentName());

            //每日打卡情况 ;
            List<AttendsDTO> attendsList = handleAttendPunchEveryDay(dateStrList, attendPunches);
            attendPunchDTO.setAttendsList(attendsList);

            return attendPunchDTO;
        }).collect(Collectors.toList());


        return punchDTOS;
    }




    /**
     * 查询员工每一天的考勤打卡情况
     * @param dateStrList
     * @param attendPunches
     * @return
     */
    private List<AttendsDTO> handleAttendPunchEveryDay(List<String> dateStrList, List<AttendPunch> attendPunches) {

        //key ------> 2021-04-01  -- List<AttendPunch> ;
        Map<String, List<AttendPunch>> map = attendPunches.stream().collect(Collectors.groupingBy(AttendPunch::getPunchDateStr));

        List<AttendsDTO> attendsDTOS = dateStrList.stream().map(dateStr -> {

            AttendsDTO attendsDTO = new AttendsDTO();
            attendsDTO.setAttendDate(dateStr);

            List<AttendPunch> punchList = map.get(dateStr);
            if(CollectionUtil.isEmpty(punchList)){
                attendsDTO.setAttendStatus("缺卡/缺卡");
            }else{
                attendsDTO.setAttendStatus(handleAttendStatus(punchList));
            }

            return attendsDTO;
        }).collect(Collectors.toList());

        return attendsDTOS;
    }



    private String handleAttendStatus(List<AttendPunch> punches) {
        String amAttendStatus = "缺卡";//上午 - 考勤
        String pmAttendStatus = "缺卡";//下午 - 考勤

        for (AttendPunch punch : punches) {
            if(punch.getPunchOnOffWork() == AttendEnums.NOON_TYPE_MORNING.value()){
                amAttendStatus = this.getPushDesc(punch.getMorningPunchType());//上午
            }else if (punch.getPunchOnOffWork() == AttendEnums.NOON_TYPE_AFTERNOON.value()){
                pmAttendStatus = this.getPushDesc(punch.getAfternoonPunchType());//下午
            }
        }

        return amAttendStatus+"/"+pmAttendStatus;
    }


    private String getPushDesc(Integer morningPunchType) {
        String desc = "";
        switch (morningPunchType){
            case 1:
                desc = AttendEnums.PUNCH_TYPE_OK.toString();
                break;
            case 2:
                desc = AttendEnums.PUNCH_TYPE_LATE.toString();
                break;
            case 3:
                desc = AttendEnums.PUNCH_TYPE_EARLY.toString();
                break;
            case 4:
                desc = AttendEnums.PUNCH_TYPE_STAYALAY.toString();
                break;
            case 5:
                desc = AttendEnums.PUNCH_TYPE_NOT_WORK_DAY.toString();
                break;
            default:
                break;
        }
        return desc;
    }


    /**
     * 导出
     * @param startTime
     * @param endTime
     */
    @Override
    public void export(String startTime, String endTime) throws IOException {
        //获取response对象 , 设置响应头信息
        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        response.setHeader("Content-Disposition","attachment;filename=attendance.xls");
        response.setContentType("application/octet-stream;charset=UTF-8");

        //查询指定时间范围内的考勤打卡数据
        List<AttendPunchDTO> punchDTOList = this.queryMembers(startTime, endTime);

        //通过EasyExcel的API生成文件, 并通过流的形式输出
        EasyExcel.write(response.getOutputStream())
                .excelType(ExcelTypeEnum.XLS)

                .registerWriteHandler(new CustomStyle())//设置列宽
                .registerWriteHandler(horizontalCellStyleStrategy())//设置字体样式

                .head(headers(punchDTOList))//表头
                .sheet("考勤列表")
                .doWrite(getData(punchDTOList));//表数据

    }



    //设置样式
    public HorizontalCellStyleStrategy horizontalCellStyleStrategy(){
        WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
        //内容策略 - 水平居中
        contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
        //设置字体
        WriteFont font = new WriteFont();
        font.setFontName("阿里巴巴普惠体");
        font.setFontHeightInPoints((short)11);
        contentWriteCellStyle.setWriteFont(font);

        HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(contentWriteCellStyle, contentWriteCellStyle);
        return horizontalCellStyleStrategy;
    }


    /**
     * 表数据
     * @param punchDTOList
     * @return
     */
    private List<List<String>> getData(List<AttendPunchDTO> punchDTOList) {
        List<List<String>> dataList = new ArrayList<>();

        if(CollectionUtil.isNotEmpty(punchDTOList)) {
            punchDTOList.stream().forEach(attendPunchDto -> {
                List<String> rowList = new ArrayList<>();

                rowList.add(attendPunchDto.getUserName());
                rowList.add(attendPunchDto.getWorkNumber());
                rowList.add(attendPunchDto.getDepartmentName());
                rowList.add(attendPunchDto.getPost());

                List<AttendsDTO> attendsList = attendPunchDto.getAttendsList();
                if(CollectionUtil.isNotEmpty(attendsList)){
                    attendsList.stream().forEach(attendsDTO -> {
                        rowList.add(attendsDTO.getAttendStatus());
                    });
                }

                System.out.println("RowList: " + rowList);

                dataList.add(rowList);
            });
        }

        return dataList;
    }


    /**
     * 表头
     * @param punchDTOList
     * @return
     */
    private List<List<String>> headers(List<AttendPunchDTO> punchDTOList) {
        List<List<String>> headers = new ArrayList<>();
        headers.add(Arrays.asList("姓名"));
        headers.add(Arrays.asList("工号"));
        headers.add(Arrays.asList("部门"));
        headers.add(Arrays.asList("职位"));

        if(CollectionUtil.isNotEmpty(punchDTOList)){
            AttendPunchDTO attendPunchDTO = punchDTOList.get(0);
            List<AttendsDTO> attendsList = attendPunchDTO.getAttendsList();

            if(CollectionUtil.isNotEmpty(attendsList)){
                attendsList.stream().forEach(attendsDTO -> {
                    headers.add(Arrays.asList(attendsDTO.getAttendDate()));
                });
            }
        }

        System.out.println("Headers: " + headers);

        return headers;
    }


    @Override
    public String generateExcelReportByCompanyId(Long companyId) {
        //获取本月的第一天 , 随后一天 --------> 时间列表 ---> 2021-04-01
        String firstDay = DateTimeUtil.getCurrentMonthFirstDay();
        String lastDay = DateTimeUtil.getCurrentMonthLastDay();

        List<Date> dateList = DateTimeUtil.getDatesBetweenTwoDate(firstDay, lastDay, DateTimeUtil.TIME_FORMAT_2);
        List<String> dateStrList = dateList.stream().map(date -> {
            return DateTimeUtil.dateToStr(date, DateTimeUtil.TIME_FORMAT_2);
        }).collect(Collectors.toList());


        //获取当前企业所有的员工
        List<SysCompanyUserDTO> companyUserDTOList = sysUserFeign.queryCompanyUser(null, companyId).getData();


        //获取所有员工考勤列表数据
        List<AttendPunchDTO> punchDTOS = this.getAttendPunchDTOS(firstDay, lastDay, dateStrList, companyUserDTOList);


        //生成Excel文件
        String folderName = "D:\\考勤数据\\"+companyId;
        if(!new File(folderName).exists()){
            new File(folderName).mkdirs();
        }

        String filename=folderName+"\\考勤报表-"+firstDay.substring(0,7)+".xls"; //2021-04-01

        EasyExcel.write(new File(filename))
                .excelType(ExcelTypeEnum.XLS)

                .registerWriteHandler(new CustomStyle())//设置列宽
                .registerWriteHandler(horizontalCellStyleStrategy())//设置字体样式

                .head(headers(punchDTOS))//表头
                .sheet("考勤列表")
                .doWrite(getData(punchDTOS));//表数据

        return filename;
    }


}
